
/*
# Copyright (c) 2022 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/

#include <assert.h>
#include "napi/native_api.h"
#include "napi/native_node_api.h"
#include "seeta/QualityAssessor.h"

#include "seeta/FaceEngine.h"
#include "seeta/Struct_cv.h"

#include <stdio.h>
#include <iostream>

#include <seeta/FaceDetector.h>
#include <seeta/FaceLandmarker.h>

#include <array>
#include <map>

#define USE_HILOG

#ifdef  USE_HILOG

#include "hilog_wrapper.h"

#define     LOGI(fmt, ...)   HILOG_INFO("[SeetaFaceApp][INFO ] [%{public}s, %{public}d]" fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__)
#define     LOGE(fmt, ...)   HILOG_INFO("[SeetaFaceApp][ERROR] [%{public}s, %{public}d]" fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__)

#else
#define HILOG_ERROR(fmt, ...)   printf("[ERROR][%s|%d]" fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__)
#define HILOG_DEBUG(fmt, ...)   printf("[DEBUG][%s|%d]" fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__)
#define HILOG_INFO(fmt, ...)    printf("[INFO ][%s|%d]" fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__)

#define     LOGI(fmt, ...)   HILOG_INFO(fmt, args...)
#define     LOGE(fmt, ...)   HILOG_ERROR(fmt, args...)
#endif

using namespace std;
using namespace cv;

// ************************************************************************************************************* //
static int GetNapiValueString(napi_env env, napi_value value, char *buff, int length)
{
    int retval = 0;
    size_t len = 0;

    if (env == nullptr || value == nullptr || buff == nullptr || length <= 0) {
        LOGE("nullptr point!");
        return -1;
    }

    retval = napi_get_value_string_utf8(env, value, nullptr, -1, &len);
    if (retval != napi_ok) {
        LOGE("napi get string length failed!");
        return -1;
    }

    if (len >= length) {
        len = length - 1;   // or return failed!
    }

    retval = napi_get_value_string_utf8(env, value, buff, len + 1, &len);
    if (retval != napi_ok) {
        LOGE("napi_get_value_string_utf8 failed!");
        return -1;
    }

    return retval;
}

static void PRINT_NAPI_VALUE(napi_env env, napi_value argv)
{
    if (env == nullptr || argv == nullptr) {
        LOGE("nullptr point!");
        return;
    }
    char buff[1024] = {0};
    if (GetNapiValueString(env, argv, buff, sizeof(buff)) != napi_ok) {
        LOGE("GetNapiValueString failed!");
        return;
    }
    LOGI("NAPI_VALUE : %{public}s\r\n", buff);
}

// ************************************************************************************************************* //
typedef struct {
    int x;
    int y;
    int w;
    int h;
} FaceRect;
#define MAX_FACE_RECT   5
static int RecognizePoint(string image_path, FaceRect *rect, int num)
{
    LOGI("");
    if (rect == nullptr) {
        cerr << "NULL POINT!" << endl;
        LOGE("NULL POINT! \n");
        return -1;
    }
    seeta::ModelSetting::Device device = seeta::ModelSetting::CPU;
    int id = 0;
    seeta::ModelSetting FD_model( "/system/usr/model/fd_2_00.dat", device, id );
    seeta::ModelSetting FL_model( "/system/usr/model/pd_2_00_pts81.dat", device, id );

	seeta::FaceDetector FD(FD_model);
	seeta::FaceLandmarker FL(FL_model);

	FD.set(seeta::FaceDetector::PROPERTY_VIDEO_STABLE, 1);

    auto frame = imread(image_path);
    seeta::cv::ImageData simage = frame;
    if (simage.empty()) {
        cerr << "Can not open image: " << image_path << endl;
        LOGE("Can not open image: %{public}s", image_path.c_str());
        return -1;
    }

    auto faces = FD.detect(simage);
    if (faces.size <= 0) {
        cerr << "detect " << image_path << "failed!" << endl;
        LOGE("detect image: %s failed!", image_path.c_str());
        return -1;
    }
    for (int i = 0; (i < faces.size && i < num); i++) {
        auto &face = faces.data[i];
        memcpy(&rect[i], &(face.pos), sizeof(FaceRect));
    }
    return faces.size;
}
// ********************************************************************************************* //
static napi_value GetRecognizePointsMethod(napi_env env, napi_callback_info info)
{
    napi_value result = nullptr;
    napi_value array = nullptr;
    LOGI("");

    if (napi_create_array(env, &array) != napi_ok) {
        LOGE("napi_create_array failed! \n");
        return result;
    }

    FaceRect *p = nullptr;
    napi_value thisVar;
    void *data = nullptr;
    
    size_t argc = 0;
    napi_value args;

    napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr);
    if (argc != 1) {
        LOGE("get napi params number failed! this napi must have one param! \n");
        return result;
    }
    napi_get_cb_info(env, info, &argc, &args, &thisVar, &data);

    char path[256] = {0};

    if (GetNapiValueString(env, args, (char *)path, sizeof(path)) < 0) {
        LOGE("GetNapiValueString failed!");
        return result;
    }
    LOGI("path : %{public}s \n", path);
    string image = path;
    p = (FaceRect *)malloc(sizeof(FaceRect) * MAX_FACE_RECT);
    int retval = RecognizePoint(image, p, MAX_FACE_RECT);
    if (retval <= napi_ok) {
        LOGE("GetNapiValueString failed!");
        free(p);
        return result;
    }
    for (int i = 0; i < retval; i++) {
        int arry_int[4] = {p[i].x, p[i].y, p[i].w, p[i].h};
        int arraySize = (sizeof(arry_int) / sizeof(arry_int[0]));
        for (int j = 0; j < arraySize; j++) {
            napi_value num_val;
            if (napi_create_int32(env, arry_int[j], &num_val) != napi_ok) {
                LOGE("napi_create_int32 failed!");
                return result;
            }
            napi_set_element(env, array, i*arraySize + j, num_val);
        }
    }
    if (napi_create_object(env, &result) != napi_ok) {
        LOGE("napi_create_object failed!");
        free(p);
        return result;
    }
    if (napi_set_named_property(env, result, "recognizeFrame", array) != napi_ok) {
        LOGE("napi_set_named_property failed!");
        free(p);
        return result;
    }

    LOGI("");
    free(p);
    return result;
}

// ************************************************************************************************ //

typedef struct {
    shared_ptr<seeta::FaceEngine> engine;
    map<int64_t, string> GalleryIndexMap;
} FaceSearchInfo;

typedef struct {
    string name;
    string path;
} RegisterInfo;

static  int FaceSearchInit(FaceSearchInfo *info)
{
    if (info == NULL) {
        info = (FaceSearchInfo *)malloc(sizeof(FaceSearchInfo));
        if (info == nullptr) {
            cerr << "NULL POINT!" << endl;
            return -1;
        }
    }

    seeta::ModelSetting::Device device = seeta::ModelSetting::CPU;
    int id = 0;
    seeta::ModelSetting FD_model( "/system/usr/model/fd_2_00.dat", device, id );
    seeta::ModelSetting PD_model( "/system/usr//model/pd_2_00_pts5.dat", device, id );
    seeta::ModelSetting FR_model( "/system/usr/model/fr_2_10.dat", device, id );

    info->engine = make_shared<seeta::FaceEngine>(FD_model, PD_model, FR_model, 2, 16);
    info->engine->FD.set( seeta::FaceDetector::PROPERTY_MIN_FACE_SIZE, 80);

    info->GalleryIndexMap.clear();

    return 0;
}

static int FaceSearchRegister(FaceSearchInfo &info, RegisterInfo &gegister)
{
    if (info.engine == nullptr) {
        cerr << "NULL POINT!" << endl;
        return -1;
    }

    seeta::cv::ImageData image = cv::imread(gegister.path);
    auto id = info.engine->Register(image);
    if (id >= 0) {
        info.GalleryIndexMap.insert(make_pair(id, gegister.name));
    }

    return 0;
}

static string FaceSearchSearchRecognizer(FaceSearchInfo &info, string filename)
{
    if (info.engine == nullptr) {
        cerr << "NULL POINT!" << endl;
        return "recognize error 0";
    }
    string name;
    float threshold = 0.7f;
    seeta::QualityAssessor QA;
    auto frame = cv::imread(filename);
    if (frame.empty()) {
        LOGE("read image %{public}s failed!", filename.c_str());
        return "recognize error 1!";
    }
    seeta::cv::ImageData image = frame;
    std::vector<SeetaFaceInfo> faces = info.engine->DetectFaces(image);

    for (SeetaFaceInfo &face : faces) {
        int64_t index = 0;
        float similarity = 0;

        auto points = info.engine->DetectPoints(image, face);

        auto score = QA.evaluate(image, face.pos, points.data());
        if (score == 0) {
            name = "ignored";
        } else {
            auto queried = info.engine->QueryTop(image, points.data(), 1, &index, &similarity);
            // no face queried from database
            if (queried < 1) continue;
                // similarity greater than threshold, means recognized
            if( similarity > threshold ) {
                name = info.GalleryIndexMap[index];
            }
        }
    }
    LOGI("name : %{public}s \n", name.length() > 0 ? name.c_str() : "null");
    return name.length() > 0 ? name : "recognize failed";
}

static void FaceSearchDeinit(FaceSearchInfo *info, int need_delete)
{
    if (info != nullptr) {
        if (info->engine != nullptr) {
        }

        info->GalleryIndexMap.clear();
        if (need_delete) {
            free(info);
            info = nullptr;
        }
    }
}

// ************************************************************************************************************************* //
// 
struct CommandStrData {
    napi_async_work asyncWork = nullptr;
    napi_deferred deferred = nullptr;

    string filename;
    string result;
    FaceSearchInfo *mFaceSearch;
};

static FaceSearchInfo g_FaceSearch;
static bool g_FaceSearchInited = false;

static napi_value FaceSearchInitMethod(napi_env env, napi_callback_info info)
{
    napi_value result = nullptr;
    LOGI("");
    if (g_FaceSearchInited) {
        LOGE("FaceSearch Has Been inited!");
        if (napi_create_int32(env, 1, &result) != napi_ok) {
            LOGE("napi_create_int32 failed!");
        }
        return result;
    }
    memset(&g_FaceSearch, 0x00, sizeof(FaceSearchInfo));
    int retval = FaceSearchInit(&g_FaceSearch);
    if (retval == napi_ok) {
        g_FaceSearchInited = true;
    }

    if (napi_create_int32(env, retval, &result) != napi_ok) {
        LOGE("napi_create_int32 failed!");
    }
    LOGI("");
    return result;
}

/**
register info
//
{
    "name":"xx",
    "sum":"xx",
    "image":{
        "xx", "xx", ...
    }
}
**/
static napi_value FaceSearchRegisterMethod(napi_env env, napi_callback_info info)
{
    LOGI("");
    int retval = napi_ok;
    napi_value result = nullptr;
    napi_value thisVar;
    napi_value argv;
    void *data = nullptr;

    size_t argc = 0;
    uint32_t sum = 0;
    RegisterInfo reg_info;

    if (g_FaceSearchInited == false) {
        LOGE("FaceSearch have not init!!\n");
        return result;
    }

    napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr);
    if (argc <= 0) {
        LOGE("get napi params number failed! this napi must have param! \n");
        return result;
    }

    napi_get_cb_info(env, info, &argc, &argv, &thisVar, &data);
    napi_value object = argv;
    napi_value value = nullptr;

    if (napi_get_named_property(env, object, (const char *)"name", &value) == napi_ok) {
        char name[64] = {0};
        if (GetNapiValueString(env, value, (char *)name, sizeof(name)) < 0) {
            LOGE("GetNapiValueString failed!");
            return result;
        }
        reg_info.name = name;
    }
    LOGI("name = %{public}s", reg_info.name.c_str());
    if (napi_get_named_property(env, object, (const char *)"sum", &value) == napi_ok) {
        
        if (napi_get_value_uint32(env, value, &sum) != napi_ok) {
            LOGE("napi_get_value_uint32 failed!");
            return result;
        }
    }
    LOGI("sum = %{public}d", sum);
    if (napi_get_named_property(env, object, (const char *)"image", &value) == napi_ok) {
        bool res = false;
        if (napi_is_array(env, value, &res) != napi_ok || res == false) {
            LOGE("napi_is_array failed!");
            return result;
        }
        for (int i = 0; i < sum; i++) {
            char image[256] = {0};
            napi_value imgPath = nullptr;
            if (napi_get_element(env, value, i, &imgPath) != napi_ok) {
                LOGE("napi_get_element failed!");
                return result;
            }
            if (GetNapiValueString(env, imgPath, (char *)image, sizeof(image)) < 0) {
                LOGE("GetNapiValueString failed!");
                return result;
            }
            reg_info.path = image;
            if (FaceSearchRegister(g_FaceSearch, reg_info) != napi_ok) {
                retval = -1;
                break;
            }
        }
    }

    if (napi_create_int32(env, retval, &result) != napi_ok) {
        LOGE("[SeetaFaceApp][%{public}s|%{public}d]napi_create_int32 failed!", __func__ ,__LINE__);
    }

    LOGI("result : %d", retval);
    return result;
}

static void FaceSearchRecognizeExecuteCB(napi_env env, void *data)
{
    CommandStrData *commandStrData = dynamic_cast<CommandStrData*>((CommandStrData *)data);
    if (commandStrData == nullptr) {
        HILOG_ERROR("nullptr point!", __FUNCTION__, __LINE__);
        return;
    }

    FaceSearchInfo faceSearch = *(commandStrData->mFaceSearch);
    commandStrData->result = FaceSearchSearchRecognizer(faceSearch, commandStrData->filename);
    LOGI("Recognize result : %s !", __FUNCTION__, __LINE__, commandStrData->result.c_str());
}

static void FaceSearchRecognizeCompleteCB(napi_env env, napi_status status, void *data)
{
    CommandStrData *commandStrData = dynamic_cast<CommandStrData*>((CommandStrData *)data);
    napi_value result;

    if (commandStrData == nullptr || commandStrData->deferred == nullptr) {
        LOGE("nullptr", __FUNCTION__, __LINE__);
        if (commandStrData != nullptr) {
            napi_delete_async_work(env, commandStrData->asyncWork);
            delete commandStrData;
        }

        return;
    }

    const char *result_str = (const char *)commandStrData->result.c_str();
    if (napi_create_string_utf8(env, result_str, strlen(result_str), &result) != napi_ok) {
        LOGE("napi_create_string_utf8 failed!", __FUNCTION__, __LINE__);
        napi_delete_async_work(env, commandStrData->asyncWork);
        delete commandStrData;
        return;
    }

    napi_resolve_deferred(env, commandStrData->deferred, result);
    napi_delete_async_work(env, commandStrData->asyncWork);

    delete commandStrData;
}

static napi_value FaceSearchGetRecognizeMethod(napi_env env, napi_callback_info info)
{
    LOGI("");
    napi_value promise = nullptr;
    napi_deferred deferred = nullptr;
    NAPI_CALL(env, napi_create_promise(env, &deferred, &promise));
    
    // 异步工作项上下文用户数据，传递到异步工作项的execute、complete之间传递数据
    CommandStrData *commandStrData = new CommandStrData {
        .asyncWork = nullptr,
        .deferred = deferred,
    };

    if (g_FaceSearchInited == true) {
        commandStrData->mFaceSearch = &g_FaceSearch;
    }

    napi_value thisVar;
    void *data = nullptr;
    
    size_t argc = 0;
    napi_value args;

    napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr);
    if (argc != 1) {
        HILOG_ERROR("get napi params number failed! this napi must have one param! \n");
        delete commandStrData;
        return nullptr;
    }
    napi_get_cb_info(env, info, &argc, &args, &thisVar, &data);

    char path[256] = {0};
    if (GetNapiValueString(env, args, (char *)path, sizeof(path)) < 0) {
        HILOG_ERROR("GetNapiValueString failed!");
        delete commandStrData;
        return nullptr;
    }

    commandStrData->filename = path;

    // 创建async work，创建成功后通过最后一个参数(commandStrData->asyncWork)返回async work的handle
    napi_value resourceName = nullptr;
    napi_create_string_utf8(env, "FaceSearchGetPersonRecognizeMethod", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(env, nullptr, resourceName, FaceSearchRecognizeExecuteCB, FaceSearchRecognizeCompleteCB,
            (void *)commandStrData, &commandStrData->asyncWork);

    // 将刚创建的async work加到队列，由底层去调度执行
    napi_queue_async_work(env, commandStrData->asyncWork);
    LOGI("");
    // 返回promise
    return promise;
}

static napi_value FaceSearchClearMethod(napi_env env, napi_callback_info info)
{
    napi_value result = nullptr;
    LOGI("");
    if (g_FaceSearchInited == false) {
        LOGE("FaceSearch Has not Been inited!");
        if (napi_create_int32(env, -1, &result) != napi_ok) {
            LOGE("napi_create_int32 failed!");
        }
        return result;
    }
    
    if (g_FaceSearch.engine != nullptr) {
        g_FaceSearch.engine->Clear();
    }
    g_FaceSearch.GalleryIndexMap.clear();

    if (napi_create_int32(env, 0, &result) != napi_ok) {
        LOGE("napi_create_int32 failed!");
    }
    LOGI("");
    return result;
}
static napi_value FaceSearchDeinitMethod(napi_env env, napi_callback_info info)
{
    if (g_FaceSearchInited == true) {
        FaceSearchDeinit(&g_FaceSearch, 0);
        g_FaceSearchInited = false;
    }
}

static napi_value Init(napi_env env, napi_value exports) {
    napi_status status;
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION("GetRecognizePoints", GetRecognizePointsMethod),
        DECLARE_NAPI_FUNCTION("FaceSearchInit", FaceSearchInitMethod),
        DECLARE_NAPI_FUNCTION("FaceSearchDeinit", FaceSearchDeinitMethod),
        DECLARE_NAPI_FUNCTION("FaceSearchRegister", FaceSearchRegisterMethod),
        DECLARE_NAPI_FUNCTION("FaceSearchGetRecognize", FaceSearchGetRecognizeMethod),
        DECLARE_NAPI_FUNCTION("FaceSearchClear", FaceSearchClearMethod),
    };

    status = napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

    assert(status == napi_ok);

    return exports;
}

NAPI_MODULE(SeetafaceApp, Init);
